作為一個開源的類神經網路模型交換格式,能夠以定義一組通用的運算元集合,並運用該集合建照計算圖是很重要的。今天的文章就先來看看要怎麼用 ONNX 提供的 Python API 來建立一個計算圖,同時我們也會提到 ONNX 以 protobuf 定義圖形,節點和模型等的規格。
我們可以依照 ONNX 定義的 protobuf 檔案來建立一個計算圖。建立的方法會由葉節點,連接葉節點作為運算元輸入的中間節點,最後到根節點。
ValueInfoProto protobuf 定義建立輸入和輸出。使用者呼叫 helper.make_tensor_value_info 函式,並依序傳入名稱,資料型態,和一個 list,list 裡的每一個元素都表示一個維度。原始碼如下:# 建立輸入 (ValueInfoProto)
X = helper.make_tensor_value_info('X', TensorProto.FLOAT, [1, 2])
# 建立輸出 (ValueInfoProto)
Y = helper.make_tensor_value_info('Y', TensorProto.FLOAT, [1, 4])
print(X)
#=>name: "X"
#type {
#  tensor_type {
#    elem_type: 1
#    shape {
#      dim {
#        dim_value: 1
#      }
#      dim {
#        dim_value: 2
#      }
#    }
#  }
#}
NodeProto protobuf 定義,使用者可以呼叫 helper.make_node函式,並依序傳入 op_type,輸入,輸出和額外的屬性。在 NodeProto 的定義上,name 和 op_type 都是用來當作節點的 id ,只不過 name 是用在計算圖中,而 op_type 則是在執行檔中可讓 IR 辨識的符號。有關 NodeProto 的幾個重要欄位,可以見下表:| 欄位 | 資料型態 | 必要 | 說明 | 
|---|---|---|---|
| name | 字串 | 非必要 | 節點的名稱,用於診斷內容 | 
| input | 字串陣列 | 必要 | 用於運算元節點的輸入名稱們。這些輸入必須是計算圖的輸入,節點的輸入或初始化輸入變數的值 | 
| output | 字串陣列 | 必要 | 該運算節點的輸出,可成為計算圖輸出的參考,或建立一個新的 ValueInfoProto葉節點(見 1)於計算圖中 | 
| op_type | 字串 | 非必要 | 運算元的 ID 用於計算圖 | | 
| domain | 字串 | 非必要 | 包括 op_type 的命名空間 | 
| attribute | AttributeProto 陣列 (見下) | 屬性名稱 | |
| doc_string | 字串 | 非必要 | 人類可理解的說明 | 
屬性的部分必須依循 AttributeProto protobuf 的定義,也是必須要有屬性名稱以及型態。屬性的值是屬於常數,是不能在執行時才藉由計算得出,這項特點與節點的輸出和輸入不同,節點的輸出和輸入的值是需要在執行時得知,且他們的命名必須要獨一無二,不可有相同命名的兩個輸入或輸出。關於 AttributeProto 的部分定義可以見下表:
| 欄位 | 資料型態 | 必要 | 說明 | 
|---|---|---|---|
| name | 字串 | 必要 | 屬性的名稱,必須要獨一無二 | 
| type | AttributeType | 屬性值的資料型態 | |
| f /floats | float/ float[] | 32 位元的浮點數 / 32 位元的浮點數陣列 | |
| i /ints | int64 / int64[] | 64 位元的整數 / 64 位元的整數陣列 | |
| s /strings | byte[]/byte[][] | UTF-8 字串 / UTF-8 字串陣列 | |
| t /tensors | Tensor / Tensor[] | 張量 / Tensor 陣列 | |
| g /graphs | Graph / Graph[] | 計算圖 / 計算圖陣列 | |
| AttributeType 定義在 AttributeProto內的一個 enum list。定義的型別總共有 13 種,除了上列的 10 種外,還包括了 UNDEFINED  (未定義),SPARSE_TENSOR 和 SPARSE_TENSORS(稀疏張量和稀疏張量陣列)。原始碼如下: | 
#建立一個節點(NodeProto)
node_def = helper.make_node(
    'Pad', # op_type
    ['X'], # 輸入
    ['Y'], # 輸出
    # 額外的屬性
    mode='constant', #名為 mode 的屬性,資料型別(AttributeType)為 STRING
    value=1.5, #名為 value 的屬性,資料型別(AttributeType)為 FLOAT
    pads=[0, 1, 0, 1], #名為 pads 的屬性,資料型別(AttributeType)為 INTS 
)
print(node_def)
# => input: "X"
#output: "Y"
#op_type: "Pad"
#attribute {
#  name: "mode"
#  s: "constant"
#  type: STRING
#}
#attribute {
#  name: "pads"
#  ints: 0
#  ints: 1
#  ints: 0
#  ints: 1
#  type: INTS
#}
#attribute {
#  name: "value"
#  f: 1.5
#  type: FLOAT
#}
GraphProto protobuf 建立計算圖,使用者可以呼叫 helper.make_graph 函式並依序傳入,一個  內裝一到多個 NodeProto 物件的 python list,名稱,一個內裝一到多個輸入的 python list 和一個內裝一到多個輸出的 python list。關於 GraphProto 的部分欄位定義如下:| 欄位 | 資料型態 | 必要 | 說明 | 
|---|---|---|---|
| name | 字串 | 非必要 | 計算圖的名字 | | 
| node | Node[] | NodeProto 陣列,部分排列,可表示輸入和輸出的資料相依關係 | |
| initializer | Tensor[] | 會以預設值初始化相同名字的張量,而以常數初始化名字不同的張量 | |
| input | ValueInfo[] | 計算圖的輸入參數,可以initializer初始化 | |
| output | ValueInfo[] | 計算圖的輸出參數 | |
| value_info | ValueInfo[] | 紀錄維度和型態的資訊 | 
原始碼如下:
# 建立計算圖形 (GraphProto)
graph_def = helper.make_graph(
    [node_def],
    'test-model',
    [X],
    [Y],
)
print(graph_def)
# =>node {
# ...省略,輸出如 print(node_def)
#} input {
# ...省略,輸出如 print(X)
# } output {
# ...省略,輸出如 print(Y)
#
ModelProto protobuf 建立模型,使用者可以呼叫 helper.make_model函式並依序傳入,GraphProto 物件和指派字串給 producer_name 關鍵字引數。 producer_name 的值必須要依照使用者建造此模型所使用的深度學習架構或工具來給予。以下就是 ModelProto 常用的定義欄位:| 欄位 | 資料型態 | 必要 | 說明 | 
|---|---|---|---|
| ir_version | int64 | 非必要 | 此模型使用的 ONNX version | 
| opset_import | OperatorSetId | 必要 | 這個模型使用的 Opset 版本 | 
| producer_name | 字串 | 非必要 | 產生模型的架構名稱 | 
| producer_version | 字串 | 非必要 | 產生模型的架構版本 | 
| domain | 字串 | 一個 reverse-DNS 命名用來給予模型命名空間 | |
| model_version | int64 | 模型版本 | |
| graph | GraphProto | 非必要 | 這個模型所使用的計算圖 | 
# 建立模型 (ModelProto)
model_def = helper.make_model(graph_def,
                              producer_name='onnx-example')
print(model_def)
#=>ir_version: 6
#producer_name: "onnx-example"
#graph {
#...省略,與 print(graph_def)一樣 
#}
# opset_import {
#  version: 11
#}
檢查 ONNX 模型:呼叫 onnx.checker.check_model 即可
# 檢查
onnx.checker.check_model(model_def)
print('The model is checked!’)
在執行期間做維度臆測:在下面的原始碼中,我們將會建構一個簡單的 ModelProto 物件,使用 onnx.shape_inference 模組函式 infer_shapes 來做輸出張量的維度臆測。這次建立的計算圖,會用 make_node 建立兩個運算元 Transpose 的計算節點,關鍵值引數 perm 則是第一個輸入張量 Transpose 的維度。在計算圖中的輸入 X 和最終的輸出 Z 都在建立圖時用 make_tensor_value_info 方法來建立,所以無需做維度臆測。然而中繼變數 Y,則可以透過計算而得知,或由 X 和 Z 的維度進行臆測。下面的程式碼,即是使用後者:
from onnx import shape_inference
from onnx import TensorProto
# 前處理,建立兩個計算節點,其中 Y 的維度是未知的
node1 = helper.make_node('Transpose', ['X'], ['Y'], perm=[1, 0, 2])
node2 = helper.make_node('Transpose', ['Y'], ['Z'], perm=[1, 0, 2])
graph = helper.make_graph(
    [node1, node2],
    'two-transposes',
    [helper.make_tensor_value_info('X', TensorProto.FLOAT, (2, 3, 4))],
    [helper.make_tensor_value_info('Z', TensorProto.FLOAT, (2, 3, 4))],
)
original_model = helper.make_model(graph, producer_name='onnx-examples')
# 尚未應用維度臆測前,Y 的維度是未知
print('Before shape inference, the shape info of Y is:\n{}'.format(original_model.graph.value_info))
# => Before shape inference, the shape info of Y is:
[]
inferred_model = shape_inference.infer_shapes(original_model)
# 未應用維度臆測後,Y 的維度可被推測
print('After shape inference, the shape info of Y is:\n{}'.format(inferred_model.graph.value_info))
# => After shape inference, the shape info of Y is:
#[name: "Y"
#type {
#  tensor_type {
#    elem_type: 1
#    shape {
#      dim {
#        dim_value: 3
#      }
#      dim {
#        dim_value: 2
#      }
#      dim {
#        dim_value: 4
#      }
#    }
#  }
#}
#]